-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: calculate L1 TVL using batch request #37
Conversation
ZKS-75 L1 TVL
Create a Batch contract to query token balances from Get the top 30 list of token addresses and tickers by USD holdings on shared bridge from etherscan. Measure how many token balance request can be made until fail or revert.
and validate boundaries BridgeHub address: 0x303a465B659cBB0ab36eE643eA362c509EEb5213 SharedBridge address: 0xD7f9f54194C633F36CCD5F3da84ad4a1c38cB2cB AC:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried out the functionality using a real world rpc node ?
I was thinking that we maybe want to add a README.md and or an example.ts
with examples of metrics
lib usage
i will write this down into linear tasks
libs/shared/src/utils/parseUnits.ts
Outdated
@@ -0,0 +1,7 @@ | |||
import BigNumber from "bignumber.js"; | |||
|
|||
export const parseUnits = (value: bigint, decimals: number): number => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://viem.sh/docs/utilities/parseUnits#parseunits is this the same ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah you're right, i'll refactor to use: https://viem.sh/docs/utilities/formatUnits
libs/shared/src/tokens/tokens.ts
Outdated
export type TokenType = { | ||
name: string; | ||
symbol: string; | ||
coingeckoId: string; | ||
type: "erc20" | "native"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export type TokenType = { | |
name: string; | |
symbol: string; | |
coingeckoId: string; | |
type: "erc20" | "native"; | |
export type TokenType<TokenType extends "erc20" | "native"> = { | |
name: string; | |
symbol: string; | |
coingeckoId: string; | |
type: TokenType; |
libs/shared/src/tokens/tokens.ts
Outdated
export type TokenType = { | ||
name: string; | ||
symbol: string; | ||
coingeckoId: string; | ||
type: "erc20" | "native"; | ||
contractAddress: string | null; | ||
contractAddress: Address | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contractAddress: Address | null; | |
contractAddress: TokenType == "erc20" ? Address : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lmk if suggestions is what we want
libs/metrics/src/types/tvl.type.ts
Outdated
amount: number; | ||
amountUsd: number; | ||
name: string; | ||
imageUrl?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add address here ?
|
||
for (const token of tokens) { | ||
const balance = | ||
token.type === "native" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can add 2 typeguards isNativeToken
and isErc20Token
.filter((token) => !!token.contractAddress) | ||
.map((token) => token.contractAddress) as Address[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reduce
for (const token of tokens) { | ||
const balance = | ||
token.type === "native" | ||
? balances[addresses.length] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this correct ? always returning the same value for balance
using address.length
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I had the same doubt 🤔 but I think it's related with the structure of the return value of fetchTokenBalances
. If I'm understanding correctly, the last element of that bigint[]
array is a "special" value (which is being used here).
I'll write a suggestion at that method that might make things easier to understand here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the custom contract for batch requests, requests token balances + native balanceOf(address) which in L1 is eth balance as the last element of balances array, so we always return addresses + 1 (since native token doesn't have a contract itself)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmmmmm, maybe using the batch contract just for tokens would be a better approach and would make the code more clear.
then you will have something like
{...calculateTvl(getTokenBalances()),getEthBalance()}
I would also rename TokenTvl
to AssetTvl
Both calculateTvl(getTokenBalances())
and getEthBalance()
would return AssetTvl
@@ -0,0 +1,2 @@ | |||
export const tokenBalancesBytecode = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have a script to generate this? is it necessary to have it as variable? maybe just plain text is good enough, given that we are not getting any typing from this value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for now, i manually copied the bytecode generated on contracts compilation (same as with the abi). i think adding a script that runs after compilation that copies the bytecode into the .ts file is too much at this stage but can be a future enhancement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regarding plain text, i didn't follow you here, what are you suggesting? the bytecode as string is needed to be passed as argument of batchRequest
on EvmProvider method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i want to automate this for the cases in which the smart contract changes, so we use the compiled output each time we want to run or deploy or even running the pipeline.
this is kind of hardcoded, i don't like it if we have the contracts that can be compiled on the repo, maybe using the typechain
version of viem could be an option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would have to write a script or copy and move the json with compilation info but i think the json has too much extra info that is not needed. however i think this script should be written on a separate PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, i will add it to tech debt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice that you've introduced asserts 💯
tokens.map((token) => token.coingeckoId), | ||
); | ||
|
||
assert(balances.length === addresses.length + 1, "Invalid balances length"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why is this assert
necessary here with the + 1
? If it's related to the natspec for the fetchTokenBalances
method, this assert
might be better located inside the method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
for (const token of tokens) { | ||
const balance = | ||
token.type === "native" | ||
? balances[addresses.length] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I had the same doubt 🤔 but I think it's related with the structure of the return value of fetchTokenBalances
. If I'm understanding correctly, the last element of that bigint[]
array is a "special" value (which is being used here).
I'll write a suggestion at that method that might make things easier to understand here.
returnAbiParams, | ||
); | ||
|
||
return balances as bigint[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suggesting this purely from a pure code perspective, I've got no idea about the bussiness logic behind this so feel free to tell me that I'm absolutely wrong lol
Trying to make things a little bit more declarative, the fetchTokenBalances
could return some kind of object that distinguishes between the eth balance and the rest of the balances as it seems that the eth balance is a pretty particular case for the consumers of this data in your code:
return balances as bigint[]; | |
return { ethBalance: balances[balances.length], addressesBalances: balances.slice(0, -1) } |
Something like that might make things easier to use: you can change the assertion I've commented on by dropping the + 1
that feels strange at first sight and also, there will be no "obscure" accesses to balances[balances.length]
in the future, as it might bring some confusion to the reader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this approach looks quite better and its better than the one that i've pointed out in my prev comment.
@@ -78,9 +118,77 @@ describe("L1MetricsService", () => { | |||
}); | |||
|
|||
describe("l1Tvl", () => { | |||
it("return l1Tvl", async () => { | |||
it("return the TVL on L1 Shared Bridge", async () => { | |||
const mockBalances = [60_841_657_140641n, 135_63005559n, 12_3803_824374847279970609n]; // Mocked balances |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any special reason to have 135_63005559n
instead of 13_563_005_559n
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is to now where the decimal part starts on the bigint 😅 , do you think this is misleading?
for (const token of tokens) { | ||
const balance = | ||
token.type === "native" | ||
? balances[addresses.length] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmmmmm, maybe using the batch contract just for tokens would be a better approach and would make the code more clear.
then you will have something like
{...calculateTvl(getTokenBalances()),getEthBalance()}
I would also rename TokenTvl
to AssetTvl
Both calculateTvl(getTokenBalances())
and getEthBalance()
would return AssetTvl
returnAbiParams, | ||
); | ||
|
||
return balances as bigint[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this approach looks quite better and its better than the one that i've pointed out in my prev comment.
I've refactored taking into account your suggestions and code is definitely cleaner now 🤝 |
const balance = isNativeToken(token) | ||
? balances.ethBalance | ||
: balances.addressesBalance[ | ||
addresses.indexOf(tokenInfo.contractAddress as Address) | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💎
} | ||
|
||
// we assume the rounding error is negligible for sorting purposes | ||
tvl.sort((a, b) => Number(b.amountUsd) - Number(a.amountUsd)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good clarifying comment there 💯 , just one extremely nitpick question but figured is worth asking: this might cause tvl
to have elements with equal amountUsd
to be sorted differently even if run with the same set of data [1]. Are we ok with that?
Eg, this is potentially possible:
> tvl.sort()
[{name: "A", amountUsd: 1}, {name: "B", amountUsd: 2}]
> tvl.sort()
[{name: "B", amountUsd: 1}, {name: "A", amountUsd: 2}]
It seems that the JavaScript engines now generally tend to implement the sort
function in a stable way, but there's a second thing to have in mind also: the order of the returned tvl
, if the sort
is stable, will probably depend on the order of the input tokens
.
To wrap up, is the order of tvl
critical? If yes, you might also sort by name
(when amountUsd
values are equal) or something like that to always have a consistent order, without depending on the sort
implementation or the tokens
order. If this is not the case, this works perfectly. 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this is something we might not need to care about for now, but it's a good observation. Let's keep it as it is for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think that the chances of two tokens to have the same tvl is really low on the most relevant tokens, this is more likely the situation on token with low balances or 0ish TVL
adding a second sort by name i think is not necessary for now (we don't care if PEPE goes before or after MAGAIBA xd)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hahahahaha , nigiri is right. However we should be aware of this if we need to change it in the future. :)
@@ -0,0 +1,2 @@ | |||
export const tokenBalancesBytecode = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, i will add it to tech debt
🤖 Linear
Closes ZKS-75
Description
Retrieve L1 TVL of Shared Bridge from static token list: